package de.stefanocke.roo.wicket.base

import de.stefanocke.roo.wicket.ValidationException
import de.stefanocke.roo.wicket.model.BeanPropertyMetadata
import de.stefanocke.roo.wicket.model.BeanPropertyMetadataWithDetails
import de.stefanocke.roo.xtend.Parameter
import java.util.HashMap
import java.util.List
import org.springframework.roo.classpath.details.MethodMetadata
import org.springframework.roo.model.JavaType

import static de.stefanocke.roo.wicket.base.BeanPropertiesAwareItdBuilder.*
import static de.stefanocke.roo.wicket.base.WicketItdBuilder.*
import static de.stefanocke.roo.xtend.Method.*
import static extension de.stefanocke.roo.xtend.MethodSignature.*
import static extension de.stefanocke.roo.wicket.base.StandardParam.*

import static extension de.stefanocke.roo.xtend.RooExtensions.*
import static extension org.springframework.roo.classpath.details.BeanInfoUtils.*
import de.stefanocke.roo.wicket.Stereotypes
import java.util.Map
import java.util.Set
import org.springframework.roo.classpath.details.ConstructorMetadata
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails
import de.stefanocke.roo.xtend.Method
import org.springframework.roo.classpath.details.InvocableMemberMetadata
import java.lang.reflect.Modifier

class BeanPropertiesAwareItdBuilder<PCM extends ComponentMetadata> extends WicketItdBuilder implements PropertyFileChanges {
	
	public static String PROPERTY_MODEL = "org.apache.wicket.model.PropertyModel"
	
	override BeanPropertiesAwareItdMetadataBase getMetadata(){super.metadata as BeanPropertiesAwareItdMetadataBase} 
	
	def List<BeanPropertyMetadataWithDetails<PCM>> propertiesForScaffolding(){metadata.propertiesForScaffolding}
	
	def <C extends ComponentMetadata> getCreateComponentMethods(List<BeanPropertyMetadataWithDetails<C>> properties
	){
		
		val methodsForProperty = [BeanPropertyMetadataWithDetails<C> p |
			
			val name = p.getPropertyName
					
			getCreateComponentMethods(p, name.toString)
			
		]
		properties.map(methodsForProperty).flatten
		
	}
	
	
	
	
	def getCreateComponentMethods(BeanPropertyMetadataWithDetails p, String id){
		 
		val cm = p.componentMetadata
		val List<Method> methods = newArrayList();
		val baseMethodName = p.getCreateComponentMethodName
		
		val listComponentClass = getListComponentClass(p);
		
		val createList= p.collection && listComponentClass != null /*&& !cm.supportsCollection*/ 
		
		if(cm!=null){
			val compType =  cm.destination 
					
			val paramValues1 = newHashMap(ID -> "id", MARKUPPROVIDER -> "this")
			//constructor call for component	
			methods.add(
				method(signature(compType.bindTypeParam(p.type), baseMethodName+"Bare", ID.toParam),
				[|
					'''return «getConstructorCall(compType, cm.constructors, paramValues1)»;'''
				])
			)
			
			if(cm.needsDecoration && decoratorClass != null){
				val decType = decoratorClass.type
				val paramValues2 = newHashMap(ID -> "id", MARKUPPROVIDER -> "this", INLIST -> createList.toString)
				val delegate2 = methods.last 
				methods.add(
					method(signature(decType.bindTypeParam(p.type), baseMethodName+"Decorated", ID.toParam),
					[|
						'''return «getCreateAnonymousClassWithSingleMethodOverride(decoratorClass, decoratorMethodToOverride, delegate2, paramValues2, p.type)»;'''
					])
					//TODO: Beim Konstruktor fehlt das type arg...
				)
			}
		
		} else {
			//no custom component found. Use one of the factory methods defined in FormFieldsPanel
			val factoryMethod = getBestMatchingFactoryMethod(p.type, p.getTypeDetails, p.getEffectiveAnnotations.map[it.annotationType])
			//the parameters after the ID param
			val additionalParams = factoryMethod.params.tail
			
			methods.add(
				method(signature(factoryMethod.returnType.bindTypeParam(p.type), baseMethodName+"Bare", ID.toParam), [|'''
					«FOR param : additionalParams»
						«assignFactoryMethodParam(p, param, factoryMethod)»
					«ENDFOR»
					return «factoryMethod.methodName»(id«FOR param : additionalParams», «param.name»«ENDFOR»);
				'''])	
			)
			
		}
		
		if(createList){
			val listType = listComponentClass.type
			val paramValues3 = newHashMap(ID -> "id", MARKUPPROVIDER -> "this")
			val delegate3 = methods.last 
			methods.add(
				method(signature(listType.bindTypeParam(p.type), baseMethodName+"List", ID.toParam),
				[|
					'''return «getCreateAnonymousClassWithSingleMethodOverride(listComponentClass, 
						listComponentClass.listComponentMethodToOverride, delegate3, paramValues3, p.type)»;'''
				])
				//TODO: Beim Konstruktor fehlt das type arg...
			)
		}
		
		//method that knows the id of the property
		
		val paramValues4 = newHashMap(ID -> '''"«id»"''')
		val delegate4 = methods.last 
		methods.add(
			
			method(signature(delegate4.returnType, baseMethodName),
			[|{
				
				'''return («delegate4.returnType.resolve») «delegate4.name»(«getArgumentList(delegate4.parameters, paramValues4)»);'''
			}])
			//TODO: Der Type-Cast is nur wegen eines AspectJ bugs erforderlich, und auch nur, wenn der wrapper eine inner class ist 
			//Siehe https://bugs.eclipse.org/bugs/show_bug.cgi?id=384398
		)
		
		methods
	}
	
	def getCreateComponentMethodSignature(BeanPropertyMetadata p, JavaType returnType){
		
		signature(returnType.bindTypeParam(p.type), p.getCreateComponentMethodName)
	}
	
	def ClassOrInterfaceTypeDetails getDecoratorClass(){
		null
	}
	
	def MethodMetadata getDecoratorMethodToOverride(){
		null
	}
	
	def ClassOrInterfaceTypeDetails getListComponentClass(BeanPropertyMetadataWithDetails p){
		null
	}
	
	def MethodMetadata getListComponentMethodToOverride(ClassOrInterfaceTypeDetails listComponentClass){
		null
	}
	
	
	/**
	 * We currently support only one type param and assume it's the type of the property. 
	 */
	def bindTypeParam(JavaType rawType, JavaType typeArg){
		var type = rawType
		if(type.parameters.size == 1){
			//if the return type has one type argument we expect it to be the type of the property
			type = type.fqn.javaType(typeArg.wrapIfPrimitive)
		}
		if(type.parameters.size > 1){
			throw new ValidationException("Return type "+rawType.nameIncludingTypeParameters+" has more than one type parameter.");
		}
		type
	}
	
	
	
	
	
	
	def getCreateAnonymousClassWithSingleMethodOverride(ClassOrInterfaceTypeDetails superType, MethodMetadata methodToOverride, 
		Method delegateMethod, Map<StandardParam,? extends CharSequence> paramValues, JavaType typeArg){
		'''«getConstructorCall(superType, paramValues, typeArg)»{
			public «methodToOverride.returnType.resolve» «methodToOverride.methodName»(«FOR p: methodToOverride.parameters SEPARATOR ','»«p.type.resolve» «p.name»«ENDFOR»){
				return «delegateMethod.name»(«FOR p: delegateMethod.signature.parameters SEPARATOR ', '»«p.name»«ENDFOR»);
			}			
		}'''
	}
		
	
	def getConstructorCall(ClassOrInterfaceTypeDetails cid, Map<StandardParam,? extends CharSequence> paramValues, JavaType typeArg){
		getConstructorCall(cid.type.bindTypeParam(typeArg), cid.declaredConstructors, paramValues)
	}
		
	def getConstructorCall(ClassOrInterfaceTypeDetails cid, Map<StandardParam,? extends CharSequence> paramValues){
		getConstructorCall(cid.type, cid.declaredConstructors, paramValues)
	}
	
	def getConstructorCall(JavaType type, Iterable<? extends ConstructorMetadata> constructors, Map<StandardParam,? extends CharSequence> paramValues){
		val constr = findMatchingConstructor(constructors, paramValues.keySet)
		getConstructorCall(type, constr, paramValues)
	}
		
	def findMatchingConstructor(Iterable<? extends ConstructorMetadata> constructors, Set<StandardParam> allowedParams){
		val constr = constructors.filter[parameterNames.forall[allowedParams.contains(toStandardParam(it))]].head
		if(constr==null){
			throw new IllegalArgumentException("No constructor found for "+constr.declaredByMetadataId +
			" that has only following parameters: "+allowedParams+". Found constructors: "+constructors)
		}
		constr
	}
	
	def getConstructorCall(JavaType type, ConstructorMetadata constr, Map<StandardParam,? extends CharSequence> paramValues){
		'''new «type.resolve»(«getArgumentList(constr, paramValues)»)'''
	}
	
	 
	def getArgumentList(InvocableMemberMetadata methodToCall, Map<StandardParam,? extends CharSequence> paramValues){
		getArgumentList(methodToCall.parameters, paramValues);
	}
	
	def getArgumentList(Iterable<Parameter> parameters, Map<StandardParam,? extends CharSequence> paramValues){
		//Idee: Statt des reinen Name-Matchings könnte es auch StandardParams mit wildcard name (*) geben. Bei diesen wird dann der Typ herangezogen, um zu matchen.
		//So ließen sich wie bei den factory methods Annotationen usw. übergeben.
		'''«FOR p: parameters SEPARATOR ', '»«paramValues.get(p.toStandardParam)»«ENDFOR»'''
	}
	
	
	
	
	
	
	/**
	 * Creates a method for creating a wicket compoonent for a web field.
	 * The method calls the best matching factory method and tries to assign the parameters of the factory method.
	 * The fist parameter of the factory method must always be the wicket id. After that,  there might be custom parameters see assignFactoryMethodParam. 
	 */
//	def getCreateComponentByFactoryMethod(BeanPropertyMetadataWithDetails p, String id){
//		val factoryMethod = getBestMatchingFactoryMethod(p.type, p.getTypeDetails, p.getEffectiveAnnotations.map[it.annotationType])
//				//the parameters after the ID param
//				val additionalParams = factoryMethod.params.tail
//				
//				method(getCreateComponentMethodSignature(p, factoryMethod.returnType) ,[|'''
//					«FOR param : additionalParams»
//						«assignFactoryMethodParam(p, param, factoryMethod)»
//					«ENDFOR»
//					return «factoryMethod.methodName»("«id»"«FOR param : additionalParams», «param.name»«ENDFOR»);
//				'''])	
//	}
	
	//for cases where it is necessary to use an indirection via ComponentFactory to create a custom component
	def <C extends ComponentMetadata> getCreateComponentFactoryMethods(List<BeanPropertyMetadataWithDetails<C>> properties
	){
		properties.filter[hasComponentMetadata].map[getCreateComponentFactoryMethod]	
	}
	
	
	//	public ComponentFactory<User> PostTable.createUserColumnComponentFactory() {
	//    	return new ComponentFactory<User>() {
	//
	//			@Override
	//			public Component createComponent(String id, IModel<User> model) {
	//				return new UserShortInfo(id, model);
	//			}
	//		};
	//    }
	def getCreateComponentFactoryMethod(BeanPropertyMetadataWithDetails p){
		val cm = p.componentMetadata 
		//TODO: Find the "best" method or constructor.
		val constr = cm.getConstructors().head
		
		val compType = cm.destination 
		
		//TODO assign the parameters etc...
		val cfType = Stereotypes::COMPONENT_FACTORY.stereotype(p.type)
		method(signature(cfType, p.getCreateComponentFactoryMethodName),
			[|'''
				return new «cfType.resolve»() {
					@Override
					public «COMPONENT.resolve» createComponent(String id, «model(p.type).resolve» model) {
						return new «compType.resolve»(id, model);				
					}
				};
				
			'''])
	}
	
	
	/**
	 * Parameters have an annotation will be injected with the according field annotation, if present.
	 * Parameters of type class will be injected with the field's type.
	 * Parameters of type IModel will be injected with the model of the surrounding container component, if the generic type argument matches.
	 * TODO: What is the defintion of surrounding container component here?  If we have nested properties we might go backwards
	 * within the property path and look for the first matching model.
	 * 
	 * 
	 * TODO: This is all quick and dirty. Probably IModel search logic must be moved to the metadata provider.
	 * 
	 * TODO: Rewrite that all ;)
	 */
	def assignFactoryMethodParam(BeanPropertyMetadata property, Parameter param, MethodMetadata factoryMethod){
		val type = param.type
		val lhs = '''«param.type.resolve» «param.name»'''
		if(type.fqn.equals(JavaType::CLASS.fqn)){
			//Model object class.
			//we do not check for appropriate type argument. That's factory method's author's responsibility...
			'''«JavaType::CLASS.fqn.javaType(property.type.wrapIfPrimitive).resolve» «param.name» = «property.type.classConstant»;'''
		}  	
	else if(type.fqn.equals(IMODEL)){
			//Inject the Model, if the model object type matches.
			val reqestedModelClass = if(type.parameters.empty) null else type.parameters.head
			if(modelClass.equals(reqestedModelClass)){
				'''«lhs» = getModel();''' }
			else { //context injection is subject to the bindings now...
//				val candidates = modelClassTypeDetails?.memberDetails?.methods
//					.filter[it.isAccessorMethod]
//					.filter[it.returnType?.equals(reqestedModelClass)]
//					
//				var candidatesWithMatchingName = candidates.filter[param.name?.symbolName.equals(it.propertyNameForJavaBeanMethod.symbolName.toFirstLower)]
//				
//				val candidate = 
//					if(!candidatesWithMatchingName?.empty) {
//						candidatesWithMatchingName.head 
//					} else if (!candidates?.empty && !reqestedModelClass.coreType) {
//						if(candidates.size>1){
//							throw new ValidationException("More than one candidate properties of model class "+ modelClass +" found for parameter "
//								+param+ "in factory method "+factoryMethod.methodName+ " : "+candidates.map[it.propertyNameForJavaBeanMethod.symbolName.toFirstLower]
//							);
//						}
//						//if there is only one property of right type, use it.
//						//but only, if it's not a core type. (For example, we do not want to inject a String property just because it's the only one.)
//						candidates.head	
//					} else {
//						null;
//					}	
//				
//				if(candidate!=null){
//					'''«lhs» = new «PROPERTY_MODEL.javaType(reqestedModelClass).resolve»(getModel(),"«candidate.propertyNameForJavaBeanMethod.symbolName.toFirstLower»");'''
//				} else {
//					'''«lhs» = null;'''   		
//					//TODO: Validation error if the parameter is required? But then, this method probably should have been filtered before...
//					//Or: If no matching model is found,then "propagate upwards". That is: Add a required constructor argument to the surrounding 
//					//component.
//				}
			}
		}
		else if(property.getAnnotationOfType(param.type)!=null){
			//inject the annotation.
			'''«lhs» = «property.getFieldConstantGovernor.resolveStatic».«property.getFieldAnnotationsConstantName».get(«param.type.resolveStatic».class);'''
		} else if("AnnotationMap".equals(param.type.simpleTypeName)){
			//TODO: Use Stereotype to determine fqn of the type.
			'''«lhs» = «property.getFieldConstantGovernor.resolveStatic».«property.getFieldAnnotationsConstantName»;'''
		
		} 
		else {
			if(param.type.isBoolean){
				'''«lhs» = false;'''  
			} else {
				'''«lhs» = null;'''
			} //TODO: Numeric types
			
		}
	}
	
	
	
	
	def String getCreateComponentMethodName(BeanPropertyMetadata p){}
	
	def String getCreateComponentFactoryMethodName(BeanPropertyMetadata p){}

	override getPropertiesToAdd() {
		val result = new HashMap<String, String>()
		propertiesForScaffolding.forEach(p | 
			//TODO: Support nestes properties ... (key is property 'path')
			result.put(p.propertyName.symbolName, p.propertyName.readableSymbolName)
		)
		result
	}
	
	override getPropertiesToRename() {
		null
	}
	
	//	protected Class<Part> getObjectClass() {
	//		return Part.class;
	//	};
	def getObjectClassMethod(){
		method(
			signature(
				Modifier::PUBLIC, JavaType::CLASS.fqn.javaType(modelClass), "getObjectClass"
			),
			[|'''
			return «modelClass.resolveStatic».class;
			''']
			
		)
	}
	
}